################################################################################
#
# Copyright 2016 Intel Corporation
#
#
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
################################################################################

##
# 'libcoap' script to make sure LibCoAP library is "installed"
##

import os
import subprocess
import SCons.Util

Import('env')

target_os = env.get('TARGET_OS')
src_dir = env.get('SRC_DIR')
build_dir = env.get('BUILD_DIR')
ca_transport = env.get('TARGET_TRANSPORT')
with_tcp = env.get('WITH_TCP')
with_upstream_libcoap = env.get('WITH_UPSTREAM_LIBCOAP')

# Temporary LibCoAP URL is a fork of the original.
# Once a pull request is merged, change this back to the obgm original below.
libcoap_repo_url = 'https://github.com/dthaler/libcoap'
#libcoap_repo_url    = 'https://github.com/obgm/libcoap'

######################################################################
# Check for, and possibly update libcoap from git
######################################################################
# The libcoap tag here must match the one in extlibs/libcoap/prep.sh.
libcoap_version = 'IoTivity-1.4'
libcoap_dir = os.path.join(src_dir, 'extlibs', 'libcoap', 'libcoap')

# git commands
libcoap_checkout_cmd = 'git clone ' + libcoap_repo_url + '.git extlibs/libcoap/libcoap -b ' + libcoap_version
libcoap_update_cmd = 'git fetch --tags && git checkout -f ' + libcoap_version
libcoap_tag_cmd = 'git tag -l ' + libcoap_version
libcoap_sync_cmd = 'git checkout -f ' + libcoap_version
libcoap_chdir_cmd = 'cd ' + libcoap_dir

# define msg strings here so code sequence is readable
err_nocoap = '''
*********************************** Error: ************************************
* Please download libcoap using the following command:
*     $ %s
*******************************************************************************
''' % libcoap_checkout_cmd

info_oldcoap = '''
******************************* Info: *****************************************
* Your libCoAP repo is not up to date with the latest version we require (%s).
*******************************************************************************
''' % libcoap_version

info_updatingcoap = '''
******************************* Info: *****************************************
* Automatically updating libcoap to version %s.
*******************************************************************************
''' % libcoap_version

info_updatecoap = '''
******************************* Info: *****************************************
* Please update using the following commands:
*   %s
*   %s
*******************************************************************************
''' % (libcoap_chdir_cmd, libcoap_update_cmd)

info_forkedcoap = '''
******************************* Info: *****************************************
* Using FORKED copy of libCoap located in:
* resource/csdk/connectivity/lib/libcoap-4.1.1
*******************************************************************************
'''

libcoap_env = env.Clone()
if with_upstream_libcoap == '0':
    print(info_forkedcoap)
else:
    # If using the github libcoap, check for correct version
    # Right now this script assumes the revision is a tag, not a branch or
    # an arbitrary commit. If this changes, update the check below, or else
    # the script will always conclude the repo is not up to date because a tag
    # with that name does not exist.
    print('*** Checking for installation of libCoAP %s ***' % libcoap_version)
    if not os.path.exists(libcoap_dir):
        Exit(err_nocoap)

    # Tizen uses its own process to prepare the libcoap repo in gbsbuild.sh
    # and cannot use git during the build. That process removes .git.
    # Do the git checks only if .git is present.
    if os.path.exists(libcoap_dir + '/.git/HEAD'):
        start_dir = os.getcwd()
        os.chdir(libcoap_dir)
        out = subprocess.check_output(libcoap_tag_cmd, shell=True).rstrip()
        if libcoap_version not in SCons.Util.to_String(out):
            print(info_oldcoap)
            if libcoap_env.get('AUTOMATIC_UPDATE'):
                print(info_updatingcoap)
                try:
                    retcode = subprocess.call(libcoap_update_cmd, shell=True)
                    if retcode:
                        Exit("libcoap: update failed: " + str(retcode))
                except OSError as e:
                    Exit("libcoap: execution failed: " + e)
            else:
                Exit(info_updatecoap)
        # to be pedantic, always checkout to the tag
        retcode = subprocess.call(libcoap_sync_cmd, shell=True)
        if retcode:
            Exit("libcoap: checkout failed: " + str(retcode))
        os.chdir(start_dir)

######################################################################
# Build libCoAP
######################################################################
# Build flags
if target_os not in ['windows', 'winrt', 'msys_nt']:
    # libcoap uses ipv6 features from <netinet/in.h> which only
    # turn on if __GNU_SOURCE is defined
    libcoap_env.AppendUnique(CPPDEFINES=['WITH_POSIX', '_GNU_SOURCE'])
    libcoap_env.AppendUnique(CFLAGS=['-std=gnu99','-fPIC'])

if target_os not in ['windows', 'winrt']:
    libcoap_env.AppendUnique(CFLAGS=['-Wall', '-ffunction-sections',
            '-fdata-sections', '-fno-exceptions'])

if target_os == 'msys_nt':
    libcoap_env.Replace(CCFLAGS = str(libcoap_env['CCFLAGS']).replace('/WX',''))
    libcoap_env.AppendUnique(CPPDEFINES=['_DEFAULT_SOURCE'])
    libcoap_env.AppendUnique(CFLAGS=['-std=c99'])

if target_os in ['linux', 'tizen', 'android', 'ios', 'windows']:
    if with_tcp == True:
        libcoap_env.AppendUnique(CPPDEFINES=['WITH_TCP', 'WITH_WS'])

if target_os in ['linux', 'tizen', 'android']:
    libcoap_env.AppendUnique(LIBS=['log'])
    if 'BLE' in ca_transport or 'BT' in ca_transport or 'ALL' in ca_transport:
        libcoap_env.AppendUnique(CPPDEFINES=['WITH_TCP'])

# Remove -Werror build flag when building the 'coap' library  it
# is external code. see IOT-2539
while '-Werror' in libcoap_env['CCFLAGS']: libcoap_env['CCFLAGS'].remove('-Werror')
if env['CC'] == 'cl':
    # In external code, don't fail on warnings:
    #  - warning C4267: conversion from size_t to unsigned short, possible loss of data
    #  - warning C4244: '=' : conversion from 'size_t' to 'unsigned short', possible loss of data
    # TODO: fix libcoap fork, we introduced this error, it isn't from upstream
    libcoap_env.AppendUnique(CCFLAGS=['/wd4267', '/wd4244'])

######################################################################
# Source files and Target(s)
######################################################################
if with_upstream_libcoap == '0':
    # For bring up purposes only, the forked version will live here.
    libcoap_src_root = '#/resource/csdk/connectivity/lib/libcoap-4.1.1/'
    libcoap_src = Glob(libcoap_src_root + '*.c')
else:
    # We need to generate a pair of headers to describe our config
    coap_h_pc_file = os.path.join(libcoap_dir, 'include', 'coap', 'coap.h.in')
    config_h_file = os.path.join(libcoap_dir, 'include', 'coap', 'coap_config.h')
    libcoap_env.PrependUnique(CPPPATH=['libcoap/include/coap'])
    libcoap_env.AppendUnique(CPPDEFINES=['WITH_UPSTREAM_LIBCOAP'])

    # Generate coap_config.h
    target_arch = env.get('TARGET_ARCH')
    lib_prefix = '' + str(libcoap_env.get('LIBPREFIX'))

    if not os.path.isfile(config_h_file) and env.GetOption not in ('clean', 'help'):
        if ((target_os == 'windows') and (libcoap_env.get('MSVC_UWP_APP') == '1')):
            conf = Configure(libcoap_env.Clone(LIBS=libcoap_env.get('UWP_LIBS')))
        else:
            conf = Configure(libcoap_env.Clone(LIBS=[]))

        config_h_header = '''
/* ****************************************************************************
 * coap_config.h - libcoap platform-specific configuration header.
 *
 * Auto-generated code for the %s %s platform. Do not edit.
 *
 *************************************************************************** */

#ifndef _COAP_CONFIG_H_
#define _COAP_CONFIG_H_

''' % (str(target_os), str(target_arch))

        config_h_footer = '''

/* Define to the full name of this package. */
#define PACKAGE_NAME "%s"

/* Define to the full name and version of this package. */
#define PACKAGE_STRING "%s"

#ifndef COAP_STATIC_INLINE
#  if defined(__cplusplus)
#    define COAP_STATIC_INLINE inline
#  else
#    ifdef _MSC_VER
#      define COAP_STATIC_INLINE static __inline
#    else
#      define COAP_STATIC_INLINE static inline
#    endif
#  endif
#endif

#endif // _COAP_CONFIG_H_

''' % (str(lib_prefix + 'coap'), str(lib_prefix + 'coap ' + libcoap_version))

        config_h_body = ''

        cxx_headers = [
            'arpa/inet.h',
            'assert.h',
            'limits.h',
            'netinet/in.h',
            'stdio.h',
            'strings.h',
            'sys/select.h',
            'sys/socket.h',
            'sys/time.h',
            'sys/types.h',
            'sys/uio.h',
            'sys/unistd.h',
            'syslog.h',
            'time.h',
            'unistd.h',
            'winsock2.h',
            'ws2tcpip.h'
        ]

        cxx_functions = [
            'malloc',
            'snprintf',
            'strnlen',
            'vprintf',
            'fls',
            'flsll',
        ]

        def get_define_from_string(string_):
            string_converted = string_.replace("/", "_").replace(".", "_").upper()
            return "HAVE_" + string_converted

        for header_file_name in cxx_headers:
            if conf.CheckCXXHeader(header_file_name):
                config_h_body += "#define %s 1\n\n" % get_define_from_string(
                    header_file_name)

        for function_name in cxx_functions:
            if conf.CheckFunc(function_name):
                config_h_body += "#define %s 1\n\n" % get_define_from_string(
                    function_name)

        if conf.CheckCXXHeader('windows.h'):
            config_h_body += "#define ssize_t SSIZE_T\n\n"
            config_h_body += "#define in_port_t uint16_t\n\n"

        conf.Finish()
        with open(config_h_file, "w") as configfile:
            configfile.write(config_h_header + config_h_body + config_h_footer)

    # Sanity check to ensure that the above block created the file.
    if not os.path.exists(config_h_file) and not env.GetOption('clean'):
        msg = "Error: coap_config.h file not created!"
        Exit(msg)

    # Generate coap.h from coap.h.in by substitution
    pc_vars = {
        '@PACKAGE_NAME@': lib_prefix + 'coap',
        '@PACKAGE_STRING@': lib_prefix + 'coap-' + libcoap_version,
        '@PACKAGE_URL@': libcoap_repo_url,
        '@PACKAGE_BUGREPORT@': libcoap_repo_url + '/issues',
        '@PACKAGE_VERSION@': libcoap_version
    }
    libcoap_env.Substfile(coap_h_pc_file, SUBST_DICT=pc_vars)

    libcoap_src_root = 'libcoap/src/'
    try:
        libcoap_src = Glob(libcoap_src_root + '*.c',
                           exclude=libcoap_src_root + 'coap_io_lwip.c')
    except TypeError:   # very old SCons doesn't have 'exclude' in Glob
        libcoap_src = Glob(libcoap_src_root + '*.c')
        libcoap_src = [src for src in libcoap_src if 'coap_io_lwip.c' not in src.path]

# finally ready to build:
libcoap = libcoap_env.StaticLibrary(build_dir + 'coap', libcoap_src, OBJPREFIX='libcoap_')
libcoap_env.UserInstallTargetLib(libcoap)

# set for use by other scripts:
if with_upstream_libcoap == '1':
    env.AppendUnique(LIBCOAP_INC='#/extlibs/libcoap/libcoap/include')
else:
    env.AppendUnique(LIBCOAP_INC='#/resource/csdk/connectivity/lib/libcoap-4.1.1/include')
